package com.pinfly.common.http;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.HashMap;

import org.apache.log4j.Logger;

/**
 * Implements a server to process Http requests.
 * <p>
 * This class, in combination with the <code>RequestHandler</code> class and the
 * <code>RequestProcessor</code>, interface can be used to create a simple
 * server for HTTP requests.
 * <p>
 * To create such a server, instantiate an instance of this class, specifying
 * the server name, port number, and service processor. The call the
 * {@link HttpServer#start() start} method to start the server.
 * <p>
 * When a client makes a request to the server, the server will create an
 * instance of <code>RequestHandler</code> to handle the client. That class will
 * read the request and parse the parameters, then call the specified
 * <code>RequestProcessor</code> to actually process the request. The
 * <code>RequestProcessor</code> returns the results to the
 * <code>RequestHandler</code> which constructs the appropriate HTTP headers and
 * responds to the client.
 * <p>
 * So the only code that needs to be written is the <code>RequestProcessor
 * 		</code> which actually processes the request. All of the HTTP protocol work
 * is handled by this class and the <code>RequestHandler</code> classes.
 * 
 * @see RequestProcessor
 */
public class HttpServer
{
    private static final Logger logger = Logger.getLogger (HttpServer.class);

    // Timeout on server socket. It wakes up at this interval to see if
    // the server needs to be shutdown.
    private static final int TIMEOUT = 5000;
    /** Default port to listen on */
    public static final int DEF_PORT = 80;
    /** Default server name */
    public static final String DEF_NAME = "HttpServer";

    private final int m_backlog;
    private int m_portNumber;
    protected String m_serviceName;
    private ServerSocket m_serverSocket;
    private RequestProcessor m_rp;
    private String m_handlerName;
    private Thread m_runner;
    protected boolean m_sslFlag = false;
    private volatile boolean m_stopping = false;

    /**
     * Create an HTTP server with a default name, on the default port, with a
     * default request processor. The default request processor merely prints
     * the request parameters as debug output.
     */
    public HttpServer ()
    {
        this (DEF_NAME, getDefaultProcessor (), DEF_PORT);
    }

    /**
     * Create an HTTP server with the specified name and request processor. The
     * default port will be used.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     */
    public HttpServer (String name, RequestProcessor processor)
    {
        this (name, processor, DEF_PORT);
    }

    /**
     * Create an HTTP server with the specified name, listening on the specified
     * port and with the specified request processor.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     * @param port The port on which to listen for requests. A value of 0 will
     *            let the underlying operating system determine the port number.
     *            The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     * 
     */
    public HttpServer (String name, RequestProcessor processor, int port)
    {
        this (name, processor, port, -1);
    }

    /**
     * Create an HTTP server with the specified name, listening on the specified
     * port and with the specified request processor.
     * 
     * @param name The name of the server.
     * @param processor The <code>RequestProcessor</code> that will be called to
     *            process requests received by the server
     * @param port The port on which to listen for requests. A value of 0 will
     *            let the underlying operating system determine the port number.
     *            The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     * @param queueSize The serversocket backlog size if the parameter is less
     *            than or equal to 0 the system default of 50 is used.
     * 
     */
    public HttpServer (String name, RequestProcessor processor, int port, int queueSize)
    {
        // Make sure we have all the required information and that it is
        // valid. This constructor doesn't support any defaults
        if (name == null || processor == null)
        {
            throw new IllegalArgumentException ("Must specify name and processor");
        }
        if (port < 0 || port > 65535)
        {
            throw new IllegalArgumentException ("Invalid port number");
        }

        // Save the parameters
        m_serviceName = name;
        m_rp = processor;
        m_backlog = queueSize;
        m_handlerName = m_serviceName + ":" + getPort ();
        if (port > 0)
        {
            m_portNumber = port;
        }
        else
        {
            // Port of zero means any port, but we need to know
            // which one will get used. So create a temporary socket
            // now and use whatever port it gets.
            // Don't call the createSocket method because a sub-class
            // may have overridden that method and this processing is
            // done as part of the constructor, so the sub-class may not
            // be completely set up yet.
            // And it doesn't matter if we get the right kind of socket,
            // we just need to find an available port number.
            try
            {
                ServerSocket s = new ServerSocket (0);
                m_portNumber = s.getLocalPort ();
                s.close ();
            }
            catch (IOException ioe)
            {
                logger.warn ("Creating temporary socket", ioe);
            }
        }
    }

    /**
     * Create a server socket on the specified port.
     * <p>
     * Derived classes can override this method to create different types of
     * sockets (i.e. an SSL socket).
     * 
     * @param portNumber the port number to associate with the socket. A value
     *            of 0 will let the underlying operating system determine the
     *            port number. The port number chosen can be found by calling
     *            {@link HttpServer#getPort() getPort}.
     * @return a <code>ServerSocket</code> listening for requests on the
     *         specified port.
     * @throws IllegalArgumentException if there is an error creating the socket
     */
    protected ServerSocket createSocket (int portNumber)
    {
        try
        {
            if (m_backlog > 0)
            {
                return new ServerSocket (portNumber, m_backlog);
            }
            else
            {
                return new ServerSocket (portNumber);
            }
        }
        catch (Exception e)
        {
            logger.fatal ("Failed to create server socket on port " + portNumber + ".  " + e.getMessage (), e);

            String msg = e.getMessage ();
            if (msg == null || msg.length () == 0)
            {
                msg = e.getClass ().getName ();
            }
            throw new IllegalArgumentException ("Failed to create socket due to " + msg);
        }
    }

    /**
     * Set the time out value on the server socket. This value controls how
     * often the server checks to see if it should shut down. Lower values will
     * make the server shutdown quicker, at the cost of higher overhead. Higher
     * values are more efficient, but will make the server take longer to
     * shutdown.
     * <p>
     * The default value is 5000 (5 seconds).
     * 
     * @param mSec the timeout value, in milliseconds. Must be &gt; 0.
     */
    public void setTimeout (int mSec)
    {
        if (mSec <= 0)
        {
            throw new IllegalArgumentException ("Timeout must be > 0");
        }
        try
        {
            if (m_serverSocket != null)
            {
                m_serverSocket.setSoTimeout (mSec);
            }
        }
        catch (SocketException se)
        {
            logger.fatal ("Unable to set socket timeout: " + mSec, se);
        }
    }

    /**
     * This method stops the server socket so that it can not accept new
     * messages, but does not interrupt messages that are in-progress. It does
     * not replace calling the stop method {@link HttpServer#stop stop}. No
     * additional requests will be processed unless the server is restarted by
     * calling {@link HttpServer#start start}.
     * 
     * This method was added for the Enterprise Context Manager so that the
     * servers could be taken off-line and later be placed back online without
     * interrupting requests that are in-progress.
     * 
     */
    public synchronized void closeServerSocket ()
    {

        if (m_runner != null)
        {
            m_runner.interrupt ();
        }

        m_stopping = true;

        if (m_serverSocket != null)
        {
            try
            {
                // This will cause m_serverSocket.accept to
                // throw an exception. The m_stopping variable tells
                // it to ignore logging of that exception.
                m_serverSocket.close ();

                m_serverSocket = null;
            }
            catch (IOException ioe)
            {
                logger.warn ("Closing server socket", ioe);
            }
        }
    }

    /**
     * Start the server. Causes the server to start listening for requests.
     * 
     * @return <code>true</code>
     */
    public synchronized boolean start ()
    {
        // Need to stop first
        // in case only the serverSocket was closed.
        stop ();

        if (m_runner == null)
        {
            // Create the server socket
            m_serverSocket = createSocket (m_portNumber);
            setTimeout (TIMEOUT);

            RequestHandler.startServer (m_handlerName, this);
            m_runner = new Thread (new HttpServerListener (this));
            m_runner.start ();
        }
        return true;
    }

    /**
     * Stop the server. No additional requests will be processed unless the
     * server is restarted by calling {@link HttpServer#start start}.
     */
    public synchronized void stop ()
    {
        // Interrupt the service thread and tell the RequestHandler to
        // stop handling any of our requests.
        if (m_runner != null)
        {
            if (m_runner.isAlive ())
            {
                m_runner.interrupt ();
            }
            RequestHandler.stopServer (m_handlerName, this);

            m_stopping = true;

            try
            {
                if (m_serverSocket != null)
                {
                    try
                    {
                        // This will cause m_serverSocket.accept to
                        // throw an exception. The m_stopping variable tells
                        // it to ignore logging of that exception.
                        m_serverSocket.close ();
                    }
                    catch (IOException ioe)
                    {
                        logger.warn ("Closing server socket", ioe);
                    }
                }

                // Wait for the service thread to go away
                while (m_runner.isAlive ())
                {
                    try
                    {
                        m_runner.join ();
                        break;
                    }
                    catch (InterruptedException ie)
                    {
                    }
                }
            }
            finally
            {
                m_stopping = false;
            }

        }

        m_runner = null;
        m_serverSocket = null;
    }

    /**
     * Returns the status of the HttpServer
     * 
     * @return True if the HttpServer is running, otherwise false.
     */
    public synchronized boolean isRunning ()
    {
        if (m_runner != null)
        {
            return m_runner.isAlive ();
        }
        return false;
    }

    /**
     * Incoming Queue size
     * 
     * @return queue size
     */
    public int getIncomingQueueSize ()
    {
        return m_backlog;
    }

    /**
     * Get the port number that the server is using
     * 
     * @return the port number
     */
    public int getPort ()
    {
        return (m_portNumber);
    }

    /**
     * Get a default RequestProcessor.
     * <p>
     * This request processor merely reports the input parameters to the log
     * output by calling <code>Logger.debug</code> with a debug level of 3 and
     * then returns a response of "<code>empty=</code>".
     * <p>
     * It may be useful for debugging purposes.
     * 
     * @return a default <code>RequestProcessor</code>
     */
    public static RequestProcessor getDefaultProcessor ()
    {
        return new RequestProcessor ()
        {
            public String processRequest (String reference, HashMap parameters, ClientConnection clntConn)
            {
                logger.trace ("Serving: " + reference + " " + parameters);
                return "empty=";
            }
        };
    }

    /**
     * Validate and return information about the client connection. The default
     * implementation returns an ClientConnection instance that does not use SSL
     * and has no other client information. If the socket is invalid (which it
     * never is here, but can be in sub class implementations, an
     * MSHttpException is thrown
     */
    ClientConnection getClientInfo (Socket s) throws MSHttpException
    {
        return new ClientConnection (s);
    }

    /**
     * Class that actually listens for requests and processes them.
     */
    private class HttpServerListener implements Runnable
    {
        private HttpServer m_server;

        private HttpServerListener (HttpServer server)
        {
            m_server = server;
        }

        public void run ()
        {
            try
            {
                InetAddress me = InetAddress.getLocalHost ();

                logger.trace ("HttpServer (" + m_serviceName + ") at " + me.getHostAddress () + ":" + getPort ()
                              + " started");

            }
            catch (UnknownHostException uhe)
            {
                // How can local host be unknown?
            }

            // accept incoming requests
            ClientConnection clntInfo = null;
            while (!Thread.interrupted ())
            {
                Socket s = null;
                try
                {
                    s = m_serverSocket.accept ();
                    logger.trace ("New client connection");

                    try
                    {
                        clntInfo = m_server.getClientInfo (s);
                    }
                    catch (MSHttpException e)
                    {
                        // Socket failed validation - don't use it
                        logger.trace ("Connection failed validation - closing", e);
                        s.close ();
                        continue;
                    }

                    // handle the request
                    RequestHandler.serviceRequest (m_rp, s, m_handlerName, clntInfo);
                }
                catch (InterruptedIOException ioe)
                {
                }
                catch (Exception e)
                {
                    if (!m_stopping)
                    {
                        logger.fatal ("Main HttpServer thread exception " + e.getMessage (), e);
                    }

                    if (s != null)
                    {
                        try
                        {
                            s.close ();
                        }
                        catch (IOException e2)
                        {
                        }
                    }
                }
            }
        }
    }

    // // Variables shared between the test main() method and
    // // the TestClient class
    // static String url;
    // static final int numArgs = 3;
    // static final int numIterations = 15;
    //
    // public static void main (String[] args)
    // {
    // // A request processor that just echos the input
    // RequestProcessor rqp = new RequestProcessor()
    // {
    // public String processRequest (String s, HashMap args)
    // {
    // StringBuffer resp = new StringBuffer();
    // for (Iterator it = args.entrySet().iterator(); it.hasNext();)
    // {
    // Map.Entry e = (Map.Entry)it.next();
    // if (resp.length() > 0) resp.append ("&");
    // resp.append ((String)e.getKey())
    // .append ("=")
    // .append ((String)e.getValue());
    // }
    // return resp.toString();
    // }
    // };
    //
    // // Create the server on a random port
    // HttpServer server = new HttpServer ("TestServer", rqp, 0);
    // int port = server.getPort();
    // System.out.println ("Server created on port " + port);
    // url = "http://localhost:" + port + "/somepage";
    //
    // HttpClient clnt = HttpClient.getHttpClient (url);
    // HttpClient clnt2 = HttpClient.getHttpClient (url);
    // String request = "?foo=bar&bar=foo";
    // String resp = null;
    //
    // server.start ();
    // server.stop();
    // server.start();
    // try
    // {
    // resp = clnt.get (request);
    // System.out.println ("First response was " + resp);
    // resp = clnt.get (request);
    // System.out.println ("Second response was " + resp);
    // server.stop ();
    // resp = clnt.get (request);
    // System.out.println ("Third response was " + resp);
    // }
    // catch (Exception e)
    // {
    // System.out.println ("Exception 1");
    // }
    // try
    // {
    // System.out.println ("Restarting server");
    // server.start ();
    // System.out.println ("Repeating request");
    // resp = clnt2.get (request);
    // System.out.println ("Response after restart was " + resp);
    // }
    // catch (Exception e)
    // {
    // System.out.println ("Exception 2");
    // e.printStackTrace ();
    // }
    // server.stop ();
    //
    //
    // for (int ii = 0; ii < 3; ii++)
    // {
    // System.out.println ("Starting run " + ii);
    // // Start the server
    // server.start();
    //
    // // Create some clients
    // int numThreads = 3;
    // Thread[] threads = new Thread[numThreads];
    // for (int i = 0; i < numThreads; i++)
    // {
    // threads[i] = new Thread (new TestClient());
    // }
    //
    // // Start the clients
    // for (int i = 0; i < numThreads; i++)
    // {
    // threads[i].start();
    // }
    //
    // // Wait for the clients to finish
    // for (int i = 0; i < numThreads; i++)
    // {
    // try
    // {
    // threads[i].join();
    // }
    // catch (InterruptedException ie)
    // {
    // }
    // }
    //
    // // Wait for the clients to close their sockets
    // System.out.println ("Waiting for clients to close");
    // try
    // {
    // Thread.sleep (7000);
    // }
    // catch (InterruptedException ie)
    // {
    // }
    // // Shutdown the server
    // System.out.println ("Stopping server");
    // server.stop();
    // }
    //
    // }
    //
    // private static class TestClient implements Runnable
    // {
    // public void run()
    // {
    // HttpClient clnt = HttpClient.getHttpClient (url);
    // String[] names = new String[numArgs];
    // String[] values = new String[numArgs];
    // try
    // {
    // for (int i = 0; i < numIterations; i++)
    // {
    // StringBuffer q = new StringBuffer("?");
    // for (int a = 0; a < numArgs; a++)
    // {
    // if (a != 0) q.append ("&");
    // names[a] = getRandString (6);
    // values[a] = getRandString (6);
    // q.append (names[a])
    // .append ("=")
    // .append (values[a]);
    // }
    // String response = clnt.get (q.toString());
    // HashMap r = HttpUtil.decodeParameters (response);
    // for (int a = 0; a < numArgs; a++)
    // {
    // if (!values[a].equals (r.get(names[a].toUpperCase())))
    // {
    // System.out.println ("Bad response: query = " + q.toString()
    // + "\n   response = " + response);
    // break;
    // }
    // }
    // }
    // }
    // catch (Exception ex)
    // {
    // System.out.println (ex.getClass().getName());
    // ex.printStackTrace();
    // }
    // }
    // }
    //
    // private static char[] chars = new char[]
    // {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
    // 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
    // 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
    // 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    // };
    // private static java.util.Random m_random = new java.util.Random();
    // private static String getRandString (int len)
    // {
    // char[] str = new char[len];
    // for (int i = 0; i < len; i++)
    // {
    // str[i] = chars[m_random.nextInt (chars.length)];
    // }
    // return new String (str);
    // }

}
